#include "propnet.h"

#include <fstream>
#include <string>
#include <regex>
#include "cpp14.h"

namespace PropNet {

std::string Component::typeString() const
{
    switch (type) {
        case CompType::base:
            return "base";
        case CompType::input:
            return "input";
        case CompType::goal:
            return "goal";
        case CompType::terminal:
            return "terminal";
        case CompType::view:
            return "view";
        case CompType::conjunc:
            return "and";
        case CompType::disjunc:
            return "or";
        case CompType::negation:
            return "not";
        case CompType::transition:
            return "transition";
        case CompType::constant:
            return "constant";
    }
}



PropNet::PropNet()
    : terminal{"terminal", CompType::terminal}
    , comp_true{"__TRUE__", CompType::constant, true}
    , comp_false{"__FALSE__", CompType::constant, false}
{
}

Component *PropNet::addAndGate()
{
    static int count = 0; // guarantees unique name for each andgate
    components.push_back(std::make_unique<Component>(std::string("and_") + std::to_string(count++), CompType::conjunc));
    return components.back().get();
}

Component *PropNet::addOrGate()
{
    static int count = 0; // guarantees unique name for each gate
    components.push_back(std::make_unique<Component>(std::string("or_") + std::to_string(count++), CompType::disjunc));
    return components.back().get();
}

Component *PropNet::addNotGate()
{
    static int count = 0; // guarantees unique name for each andgate
    components.push_back(std::make_unique<Component>(std::string("not_") + std::to_string(count++), CompType::negation));
    return components.back().get();
}

// does not have to create goal if exact same goal already exists!
Component *PropNet::addGoal(std::string const &rolename, std::string const &utilitystr)
{
    auto role = getRole(rolename);
    int utility = std::stoi(utilitystr);
    auto p = goals[role].insert({utility, nullptr});
    if (p.second) { // new element inserted
        components.push_back(std::make_unique<Component>(rolename + "_" + utilitystr, CompType::goal));
        p.first->second = components.back().get();
    }
    return p.first->second;
}

void PropNet::defineKeywordGDL(Parser::GdlKeywordProposition const *kwd, VariableReplacements const &var_replacements)
{
    std::string extrastr = kwd->extraToString(var_replacements);
    
    if (kwd->keyword == "role") {
        size_t num_roles = roles.size();
        roles.emplace(extrastr, num_roles); // role indexing starts at 0, to directly allow indexing vectors with it
        // grow vectors that are indexed by role
        inputs.push_back({});
        legals.push_back({});
        goals.push_back(std::map<int, Component*> {});
    } else if (kwd->keyword == "base") {
        std::string basename = kwd->term.toString(var_replacements);

        components.push_back(std::make_unique<Component>(basename + "__", CompType::transition));
        Component *trans = components.back().get();

        components.push_back(std::make_unique<Component>(basename, CompType::base));
        Component *base = components.back().get();
        bases.emplace(basename, base);
        
        base->sources.push_back(trans);
    } else if (kwd->keyword == "input"){
        // note the change of keyword in the proposition name! (so cannot use kwd->toString)
        std::string nodename = "does " + extrastr + " (" + kwd->term.toString(var_replacements) + ")";

        components.push_back(std::make_unique<Component>(nodename, CompType::input));
        Component *inp = components.back().get();
        views.emplace(nodename, inp);

        size_t role = getRole(extrastr);
        inputs[role].emplace(nodename, inp);
    } else if (kwd->keyword == "legal"){
        //std::string basename = "legal " + extrastr + " (" + kwd->term.toString(var_replacements) + ")";
        std::string basename = kwd->toString(var_replacements);

        components.push_back(std::make_unique<Component>(basename, CompType::constant, true));
        Component *view = components.back().get();
        views.emplace(basename, view);

        auto role = getRole(extrastr);
        legals[role].emplace(basename, view); 
    } else if (kwd->keyword == "init"){
        initials.push_back(bases.at(kwd->term.toString(var_replacements)));
    } else {
        std::cout << "nog niet gedaan..." << std::endl;
        throw;
    }
}

void PropNet::definePropositionGDL(Parser::GdlProposition *prop)
{
    std::string nodename = prop->toString();

    components.push_back(std::make_unique<Component>(nodename, CompType::constant, true));
    Component *view = components.back().get();
    views.emplace(nodename, view);
}

// get component that represents this particular keyword term. may create Component. may recurse through getComponent
// may not return null_ptr
Component *PropNet::getKeywordComponent(Parser::GdlKeywordProposition const *gdlterm,
                                        VariableReplacements const &var_replacements)
{
    if (gdlterm->keyword == "does") {
        auto rolename = gdlterm->extraToString(var_replacements);
        auto role = getRole(rolename);
        //std::string nodename = "does " + rolename + " (" + gdlterm->term.toString(var_replacements) + ")";
        std::string nodename = gdlterm->toString(var_replacements);
        return inputs[role][nodename];
    } else if (gdlterm->keyword == "true") {
        return getComponent(&(gdlterm->term), var_replacements);
    } else if (gdlterm->keyword == "next") {
        Component *comp = getComponent(&(gdlterm->term), var_replacements);
        // must create transition if does not exist yet
        if (comp->sources.empty()) {
            components.push_back(std::make_unique<Component>(comp->name + "__", CompType::transition));
            Component *trans = components.back().get();
            comp->sources.push_back(trans);
        }

        return comp->sources[0]; // assumes correct GDL where first source must be transition
    } else if (gdlterm->keyword == "goal") {
        return addGoal(gdlterm->extraToString(var_replacements), gdlterm->term.toString(var_replacements));
    } else if (gdlterm->keyword == "terminal") {
        return &terminal;
    } else if (gdlterm->keyword == "legal") {
        std::string rolename = gdlterm->extraToString(var_replacements);
        //std::string basename = "legal " + rolename + " (" + gdlterm->term.toString(var_replacements) + ")";
        std::string basename = gdlterm->toString(var_replacements);
        components.push_back(std::make_unique<Component>(basename, CompType::view));
        Component *view = components.back().get();
        views.emplace(basename, view);
        auto role = getRole(rolename);
        legals[role].emplace(basename, view);
        return view;
    } else {
        std::cout << "getKeywordComponent unhandled Gdl type!" << std::endl;
    }


    return nullptr; // not good!
}

// get component that represents this particular term. may have to create component, and may recurse
// may not return null_ptr
Component *PropNet::getComponent(Parser::GdlTerm const *gdlterm, VariableReplacements const &var_replacements)
{
    if (auto const *prop = dynamic_cast<Parser::GdlProposition const *>(gdlterm)) {
        // first check for 'distinct'
        if (prop->strs[0] == "distinct") {
            bool istrue = evaluateGDLTrue(gdlterm, var_replacements);
            return istrue ? &comp_true : &comp_false;
        }

        // not 'distinct', handle normally:
        auto c = bases.find(prop->toString(var_replacements));
        if (c != bases.end())
            return c->second;

        // if not a base prop, then has to be view prop
        return views.at(prop->toString(var_replacements));
    } else if (auto const *kwd = dynamic_cast<Parser::GdlKeywordProposition const *>(gdlterm)) {
        return getKeywordComponent(kwd, var_replacements);
    } else if (auto const *prop = dynamic_cast<Parser::GdlNot const *>(gdlterm)) {
        auto gate = addNotGate();
        gate->sources.push_back(getComponent(prop->term.get(), var_replacements));
        return gate;
    } else if (auto const *prop = dynamic_cast<Parser::GdlOr const *>(gdlterm)) {
        auto gate = addOrGate();
        for (auto const &t: prop->terms) {
            gate->sources.push_back(getComponent(t.get(), var_replacements));
        }
        return gate;
    } else if (auto const *prop = dynamic_cast<Parser::GdlAnd const *>(gdlterm)) {
        auto gate = addAndGate();
        for (auto const &t: prop->terms) {
            gate->sources.push_back(getComponent(t.get(), var_replacements));
        }
        return gate;
    } else {
        std::cout << "getComponent unhandled Gdl type!" << std::endl;
    }

    return nullptr; // not good!
}

// check if all terms are true. really, this should only check for "(distinct x y)" type of props, the rest is true
// may recurse
/// \todo currently somewhat broken. incorrect result for evaluateGDLTrue("(<= q (not p))")
bool PropNet::evaluateGDLTrue(Parser::GdlTerm const *gdlterm, VariableReplacements const &var_replacements) const
{
    if (auto const *prop = dynamic_cast<Parser::GdlProposition const *>(gdlterm)) {
        if (prop->strs[0] == "distinct") {
            std::string arg1 = Parser::isVariable(prop->strs[1]) ? var_replacements.at(prop->strs[1]) : prop->strs[1];
            std::string arg2 = Parser::isVariable(prop->strs[2]) ? var_replacements.at(prop->strs[2]) : prop->strs[2];
            return (arg1 != arg2);
        }
        return true;
    } else if (/*auto const *kwd = */dynamic_cast<Parser::GdlKeywordProposition const *>(gdlterm)) {
        return true;
    } else if (auto const *prop = dynamic_cast<Parser::GdlNot const *>(gdlterm)) {
        return !evaluateGDLTrue(prop->term.get(), var_replacements);
    } else if (auto const *prop = dynamic_cast<Parser::GdlOr const *>(gdlterm)) {
        for (auto const &t: prop->terms) {
            if (evaluateGDLTrue(t.get(), var_replacements))
                return true;  // short-circuit evaluation
        }
        return false;
    } else if (auto const *prop = dynamic_cast<Parser::GdlAnd const *>(gdlterm)) {
        for (auto const &t: prop->terms) {
            if (!evaluateGDLTrue(t.get(), var_replacements))
                return false;  // short-circuit evaluation
        }
        return true;
    } else {
        std::cout << "evaluateGDLTrue unhandled GdlTerm type!" << std::endl;
    }

    return true;
}

void PropNet::defineAssignmentGDL(Parser::GdlAssignment const *ass,
                                  VariableReplacements const &variable_replacements)
{
    Parser::GdlTerm *gdlterm = ass->terms[0].get(); // = assignment target
                                                    // ass->terms[1:end] = sources, apply and-operation to all these terms

    Component *target{ nullptr };
    if (auto *prop = dynamic_cast<Parser::GdlProposition *>(gdlterm)) {
        std::string propstr = prop->toString(variable_replacements);
        // first check if view already exists, if not, define it
        // may want to optimize if doing a double search/insert through views-map takes too much time
        if (views.count(propstr) < 1) {
            components.push_back(std::make_unique<Component>(propstr, CompType::view));
            Component *view = components.back().get();
            views.emplace(propstr, view);
        }
        target = views.at(prop->toString(variable_replacements));
    } else if (auto *kwd = dynamic_cast<Parser::GdlKeywordProposition *>(gdlterm)) {
        if ((kwd->keyword == "base") || (kwd->keyword == "input") || (kwd->keyword == "role") ||
            (kwd->keyword == "init"))
        {
            for (auto term = ass->terms.begin() + 1, last = ass->terms.end(); term != last; ++term) {
                bool eval = evaluateGDLTrue(term->get(), variable_replacements);
                if (!eval) return;  // short-circuit eval
            }
            defineKeywordGDL(kwd, variable_replacements);
            return; // assignment in this case just means defining the base/input/... prop, so nothing to be done here
                    // anymore
        } 
        target = getKeywordComponent(kwd, variable_replacements);
    }

    // add/get or-gate if this view already has some sources leading up to it
    if (!target->sources.empty()) {
        Component *prev_source = target->sources[0];
        if (prev_source->type != CompType::disjunc) {
            Component *or_gate = addOrGate();
            or_gate->sources.push_back(prev_source);
            prev_source = or_gate;
            target->sources[0] = or_gate;
        }
        target = prev_source;
    }
    // target now points to the Component to which to add the source input

    if (ass->terms.size() == 2) {
        // simple case of one assignment source: no and-gate needed
        target->sources.push_back(getComponent(ass->terms[1].get(), variable_replacements));
    } else {
        // create and-gate and put all the sources into that one
        Component *and_gate = addAndGate();
        target->sources.push_back(and_gate);
        for (auto term = ass->terms.begin() + 1, last = ass->terms.end(); term != last; ++term) {
            and_gate->sources.push_back(getComponent(term->get(), variable_replacements));
        }
    }
}


static void addToProduct(PropNet::OptionContainer::const_iterator options,
                         PropNet::OptionContainer::const_iterator options_end,
                         PropNet::VariableReplacements &map_so_far,
                         std::vector<PropNet::VariableReplacements> &product)
{
    if (options == options_end) {
        product.push_back(map_so_far);
    } else {
        // recurse
        for (auto s : options->second) {
            map_so_far[options->first] = s;
            auto options_cpy = options;
            addToProduct(++options_cpy, options_end, map_so_far, product);
        }
    }
}
// Cartesian product of all possibilities
static std::vector<PropNet::VariableReplacements> calcProduct(PropNet::OptionContainer const &options)
{
    std::vector<PropNet::VariableReplacements> product;
    PropNet::VariableReplacements temp;
    addToProduct(options.begin(), options.end(), temp, product);
    return product;
}


// this is all heavily UNoptimized, just trying to get it to work asap.
void PropNet::loadVariableSentence(Parser::GdlSentence const &sentence,
                                   std::map<std::string, std::vector<Parser::GdlTerm const *>> varmap)
{
    std::cout << std::endl;

    OptionContainer options;
    for (auto const& var: varmap) {
        std::cout << var.first << " in: " << std::endl;
        for (auto const& term: var.second) {
            std::cout << "  " << term->toString() << std::endl;

            if (auto gdlprop = dynamic_cast<Parser::GdlProposition const *>(term)) {
                findOptions(gdlprop, options);
            } else if (auto gdlkwd = dynamic_cast<Parser::GdlKeywordProposition const *>(term)) {
                findOptions(gdlkwd, options);
            }
        }
    }

    auto product = calcProduct(options); // Cartesian product of all possibilities
    // loop over all possible combinations of variables...
    for (auto m: product) {
        // first check of valid instantiation
        for (auto const &var : varmap) {
            for (auto const &term : var.second) {
                if (auto gdlprop = dynamic_cast<Parser::GdlProposition const *>(term)) {
                    if ( !propExists(gdlprop, m)) {
                        goto invalid_instantiation; // skip the rest of outermost for-loop
                    }
                } else if (auto gdlkwdprop = dynamic_cast<Parser::GdlKeywordProposition const *>(term)) {
                    if ( !propExists(gdlkwdprop, m)) {
                        goto invalid_instantiation; // skip the rest of outermost for-loop
                    }
                }
            }
        }

        std::cout << "option: ";
        for (auto v: m) {
            std::cout << v.first << " = " << v.second << ", ";
        }
        std::cout << std::endl;

        defineAssignmentGDL(dynamic_cast<Parser::GdlAssignment *>(sentence.term.get()), m);

        invalid_instantiation: ; // goto here when invalid instance
    }
}

bool PropNet::loadGDL(std::vector<Parser::Token>::const_iterator first, std::vector<Parser::Token>::const_iterator last)
{
    auto sentences = Parser::parseTokens(first, last);

    for (auto &s: sentences) {
        std::cout << s.toString();

        auto varmap = s.getVariables();
        if (!varmap.empty()){
            loadVariableSentence(s, varmap);
            std::cout << " -> ok";
        } else {
            if (auto kwd = dynamic_cast<Parser::GdlKeywordProposition*>(s.term.get())) {
                defineKeywordGDL(kwd, VariableReplacements{});
                std::cout << " -> ok";
            } else if (auto prop = dynamic_cast<Parser::GdlProposition *>(s.term.get())) {
                definePropositionGDL(prop);
                std::cout << " -> ok";
            } else if (auto ass = dynamic_cast<Parser::GdlAssignment *>(s.term.get())) {
                defineAssignmentGDL(ass, VariableReplacements{});
                std::cout << " -> ok";
            }
        }

        std::cout << std::endl;
    }

    return true; // all lies ;-)
}

void PropNet::writeDOT(std::string const &filename) const
{
    using std::endl;

    std::ofstream outf;
    outf.open(filename, (std::ofstream::out | std::ofstream::trunc));
    if (!outf.is_open()) {
        std::cout << "couldn't open \"" << filename << "\" for DOT output." << endl;
        return;
    }

    outf << "// this file is generated by Frink (GGP)" << endl << endl;

    outf << "// roles:" << endl;
    for (auto const &r : roles) {
        outf << "//    " << r.first << endl;
    }
    outf << endl;

    outf << "// init as true:" << endl;
    if (initials.empty()) {
        outf << "//   -none-" << endl;
    } else {
        for (auto const &i : initials) {
            outf << "//    " << i->name << endl;
        }
    }
    outf << endl;

    outf << "digraph propnet {" << endl;

    // first define all nodes
    for (auto const &comp : components) {
        outf << " \"" << comp->name << "\"";
        switch (comp->type) {
            case CompType::base: {
                outf << " [shape=doublecircle, style=filled, color=lightblue]";
                break;
            }
            case CompType::constant: {
                outf << " [shape=box]";
                break;
            }
            case CompType::input: {
                outf << " [shape=hexagon, style=filled, color=palegreen]";
                break;
            }
            default:
                break;
        }
        outf << "; // type = " << comp->typeString() << endl;
    }
    outf << " \"" << terminal.name << "\""
         << " [shape=house,style=filled,color=\".7 .3 1.0\"]; // type = " << terminal.typeString() << endl;
    for (auto comp : {comp_true, comp_false}) {
        outf << " \"" << comp.name << "\""
             << " [shape=box]; // type = hard-coded constant" << endl;
    }

    // then connect them
    for (auto const &comp : components) {
        for (Component const *s : comp->sources) {
            outf << " \"" << s->name << "\" -> \"" << comp->name << "\";" << endl;
        }
    }
    for (Component const *s : terminal.sources) {
        outf << " \"" << s->name << "\" -> \"" << terminal.name << "\";" << endl;
    }

    outf << "}" << endl;

}

bool PropNet::propExists(std::string const &str) const
{
    return (bases.find(str) != bases.end()) || (views.find(str) != views.end());
}

bool PropNet::propExists(Parser::GdlProposition const *gdlprop, VariableReplacements const &map) const
{
    if (gdlprop->strs[0] == "distinct")
        return true;

    return propExists(gdlprop->toString(map));
}

bool PropNet::propExists(Parser::GdlKeywordProposition const *gdlkwdprop, VariableReplacements const &map) const
{
    if ((gdlkwdprop->keyword == "true") || (gdlkwdprop->keyword == "next")) {
        return propExists(&gdlkwdprop->term, map);
    } else if ((gdlkwdprop->keyword == "does") || (gdlkwdprop->keyword == "legal")) {
        return propExists(gdlkwdprop->toString(map));
    } else if (gdlkwdprop->keyword == "role") {
        return roles.find(gdlkwdprop->extraToString(map)) != roles.end();
    }

    return false;
}


static std::string buildRegexStrFromGDLProp(Parser::GdlProposition const *gdlprop, std::vector<std::string> &captures) {
    /// \todo could be optimized to recognize multiple occurrance of variables, to find less options for "(prop ?x ?x)" 
    std::string pattern;
    for (auto const &s: gdlprop->strs) {
        if (Parser::isVariable(s)) {
            pattern += "([\\d\\w]+)";
            captures.push_back(s);
        } else {
            pattern += s;
        }
        pattern += " ";
    }
    pattern.pop_back(); // remove final space;
    return pattern;
}

template <class Map>
static void doRegexOptionSearch(Map const &map, std::regex const &expr,
                                std::vector<std::string> const &captures, PropNet::OptionContainer &options)
{
    // can use std::copy_if? or some other <algorithm> method ...
    for (auto const &p: map) {
        std::smatch sm;
        if (std::regex_match(p.first, sm, expr)) {
            for (size_t i = 1, last = sm.size(); i < last; ++i ) {
                options[ captures[i-1] ].insert( sm[i] );  // note that sm[0] is the whole match
            }
        } 
    }    
}

void PropNet::findOptions(Parser::GdlProposition const *gdlprop, PropNet::OptionContainer &options) const
{
    std::vector<std::string> captures; // names of variables for which we capture substrings in regexp
    std::string pattern { "^" }; // pattern must occur at start of string
    pattern += buildRegexStrFromGDLProp(gdlprop, captures);
    pattern += "$"; // pattern must stretch until end of string
    std::regex expr{pattern};

    doRegexOptionSearch(views, expr, captures, options);
    doRegexOptionSearch(bases, expr, captures, options);
}

void PropNet::findOptions(Parser::GdlKeywordProposition const *gdlkwd, PropNet::OptionContainer &options) const
{
    if ( (gdlkwd->keyword == "does") || (gdlkwd->keyword == "legal") ) {
        std::vector<std::string> captures; // names of variables for which we capture substrings in regexp
        std::string pattern { "^" }; // pattern must occur at start of string
        pattern += gdlkwd->keyword + " ";
        if (Parser::isVariable(gdlkwd->extra)) {
            pattern += "([\\d\\w]+)";
            captures.push_back(gdlkwd->extra);
        } else {
            pattern += gdlkwd->extra;
        }
        pattern += " (";
        pattern += buildRegexStrFromGDLProp(&gdlkwd->term, captures);
        pattern += ")$"; // pattern must stretch until end of string
        std::regex expr{pattern};

        doRegexOptionSearch(views, expr, captures, options);
    } else if ((gdlkwd->keyword == "true") || (gdlkwd->keyword == "next")) {
        findOptions(&gdlkwd->term, options);
    } else {
        if (Parser::isVariable(gdlkwd->extra)) {
            // the "extra" is always a role name
            for (auto const &r : roles) {
                options[gdlkwd->extra].insert(r.first);
            }
        }
    }
}

} // namespace PropNet